from typing import NamedTuple, Optional, Union

from dagster import check
from dagster.core.events import DagsterEvent
from dagster.core.log_manager import coerce_valid_log_level
from dagster.serdes import (
    deserialize_json_to_dagster_namedtuple,
    register_serdes_tuple_fallbacks,
    serialize_dagster_namedtuple,
    whitelist_for_serdes,
)
from dagster.utils.error import SerializableErrorInfo
from dagster.utils.log import (
    JsonEventLoggerHandler,
    StructuredLoggerHandler,
    StructuredLoggerMessage,
    construct_single_handler_logger,
)


@whitelist_for_serdes
class EventRecord(
    NamedTuple(
        "_EventRecord",
        [
            ("error_info", Optional[SerializableErrorInfo]),
            ("message", str),
            ("level", Union[str, int]),
            ("user_message", str),
            ("run_id", str),
            ("timestamp", float),
            ("step_key", Optional[str]),
            ("pipeline_name", Optional[str]),
            ("dagster_event", Optional[DagsterEvent]),
        ],
    )
):
    """Entries in the event log.

    These entries may originate from the logging machinery (DagsterLogManager/context.log), from
    framework events (e.g. EngineEvent), or they may correspond to events yielded by user code
    (e.g. Output).

    Args:
        error_info (Optional[SerializableErrorInfo]): Error info for an associated exception, if
            any, as generated by serializable_error_info_from_exc_info and friends.
        message (str): The message associated with the event. For user-generated log messages, this
            is synthesized by the Dagster logging machinery -- the original message is
            `user_message`.
        level (Union[str, int]): The Python log level at which to log this event. Note that
            framework and user code events are also logged to Python logging. This value may be an
            integer or a (case-insensitive) string member of PYTHON_LOGGING_LEVELS_NAMES.
        user_message (str): For log messages, this is the user-generated message.
        run_id (str): The id of the run which generated this event.
        timestamp (float): The Unix timestamp of this event.
        step_key (Optional[str]): The step key for the step which generated this event. Some events
            are generated outside of a step context.
        pipeline_name (Optional[str]): The pipeline which generated this event. Some events are
            generated outside of a pipeline context.
        dagster_event (Optional[DagsterEvent]): For framework and user events, the associated
            structured event.
    """

    def __new__(
        cls,
        error_info,
        message,
        level,
        user_message,
        run_id,
        timestamp,
        step_key=None,
        pipeline_name=None,
        dagster_event=None,
    ):
        return super(EventRecord, cls).__new__(
            cls,
            check.opt_inst_param(error_info, "error_info", SerializableErrorInfo),
            check.str_param(message, "message"),
            coerce_valid_log_level(level),
            check.str_param(user_message, "user_message"),
            check.str_param(run_id, "run_id"),
            check.float_param(timestamp, "timestamp"),
            check.opt_str_param(step_key, "step_key"),
            check.opt_str_param(pipeline_name, "pipeline_name"),
            check.opt_inst_param(dagster_event, "dagster_event", DagsterEvent),
        )

    @property
    def is_dagster_event(self) -> bool:
        return bool(self.dagster_event)

    def get_dagster_event(self) -> DagsterEvent:
        if not isinstance(self.dagster_event, DagsterEvent):
            check.failed(
                "Not a dagster event, check is_dagster_event before calling get_dagster_event",
            )

        return self.dagster_event

    def to_json(self):
        return serialize_dagster_namedtuple(self)

    @staticmethod
    def from_json(json_str):
        return deserialize_json_to_dagster_namedtuple(json_str)

    @property
    def dagster_event_type(self):
        return self.dagster_event.event_type if self.dagster_event else None


def construct_event_record(logger_message):
    check.inst_param(logger_message, "logger_message", StructuredLoggerMessage)

    return EventRecord(
        message=logger_message.message,
        level=logger_message.level,
        user_message=logger_message.meta["orig_message"],
        run_id=logger_message.meta["run_id"],
        timestamp=logger_message.record.created,
        step_key=logger_message.meta.get("step_key"),
        pipeline_name=logger_message.meta.get("pipeline_name"),
        dagster_event=logger_message.meta.get("dagster_event"),
        error_info=None,
    )


def construct_event_logger(event_record_callback):
    """
    Callback receives a stream of event_records. Piggybacks on the logging machinery.
    """
    check.callable_param(event_record_callback, "event_record_callback")

    return construct_single_handler_logger(
        "event-logger",
        "debug",
        StructuredLoggerHandler(
            lambda logger_message: event_record_callback(construct_event_record(logger_message))
        ),
    )


def construct_json_event_logger(json_path):
    """Record a stream of event records to json"""
    check.str_param(json_path, "json_path")
    return construct_single_handler_logger(
        "json-event-record-logger",
        "debug",
        JsonEventLoggerHandler(
            json_path,
            lambda record: construct_event_record(
                StructuredLoggerMessage(
                    name=record.name,
                    message=record.msg,
                    level=record.levelno,
                    meta=record.dagster_meta,
                    record=record,
                )
            ),
        ),
    )


register_serdes_tuple_fallbacks(
    {
        # These were originally distinguished from each other but ended up being empty subclasses
        # of EventRecord -- instead of using the subclasses we were relying on
        # EventRecord.is_dagster_event to distinguish events that originate in the logging
        # machinery from events that are yielded by user code
        "DagsterEventRecord": EventRecord,
        "LogMessageRecord": EventRecord,
    }
)
